# Strings.java

    package org.codefilarete.tool;

    import javax.annotation.Nonnegative;
    import javax.annotation.Nullable;
    import java.util.ArrayList;
    import java.util.List;
    import java.util.function.Function;

    import org.codefilarete.tool.bean.Objects;

    import static java.lang.Character.isLowerCase;
    import static java.lang.Character.isUpperCase;
    import static java.lang.Character.toLowerCase;
    import static java.lang.Character.toUpperCase;

    /**
     * @author Guillaume Mary
     */
    public abstract class Strings {

    	public static boolean isEmpty(CharSequence charSequence) {
    		return charSequence == null || charSequence.length() == 0;
    	}

    	public static <C extends CharSequence> C preventEmpty(C charSequence, C replacement) {
    		return isEmpty(charSequence) ? replacement : charSequence;
    	}

    	public static String capitalize(final CharSequence cs) {
    		return (String) doWithDelegate(cs, new DefaultNullOrEmptyDelegate() {
    			@Override
    			public CharSequence onNotNullNotEmpty(CharSequence cs) {
    				return toUpperCase(cs.charAt(0)) + cs.subSequence(1, cs.length()).toString();
    			}
    		});
    	}

    	public static String uncapitalize(final CharSequence cs) {
    		return (String) doWithDelegate(cs, new DefaultNullOrEmptyDelegate() {
    			@Override
    			public CharSequence onNotNullNotEmpty(CharSequence cs) {
    				return toLowerCase(cs.charAt(0)) + cs.subSequence(1, cs.length()).toString();
    			}
    		});
    	}

    	/**
    	 * Converts the given input to a snake case string. For instance, "HelloWorld" becomes "hello_world".
    	 * Continuous uppercase characters are converted to lowercase with an underscore prefix: "HelloWORLD" becomes "hello_world".
    	 *
    	 * @param input any {@link String}
    	 * @return a snake case version of given input {@link String}.
    	 */
    	public static String snakeCase(String input) {
    		if (input.isEmpty()) {
    			return "";
    		}
    		StringBuilder result = new StringBuilder();
    		char[] charArray = input.toCharArray();
    		char firstChar = charArray[0];
    		result.append(isUpperCase(firstChar) ? toLowerCase(firstChar) : firstChar);

    		for (int i = 1, charArrayLength = charArray.length; i < charArrayLength; i++) {
    			char c = charArray[i];
    			if (isUpperCase(c)) {
    				// we add '_' only if the previous character is lower case, and we avoid duplicating '_'
    				if (isLowerCase(charArray[i - 1]) && charArray[i - 1] != '_') {
    					result.append("_");
    				}
    				result.append(toLowerCase(c));
    			} else {
    				result.append(c);
    			}
    		}
    		if (result.length() > 0 && result.charAt(0) == '_') {
    			result.deleteCharAt(0);
    		}
    		return result.toString();
    	}

    	/**
    	 * Concatenate count (positive) times parameter s.
    	 * Optional Strings in prebuildStrings are used to speed concatenation for large count numbers if you already have
    	 * large snippets of s pre-concatenated. For instance, you want 3456 times "a" and you already got constants with
    	 * a*500, a*100, a*10, then this method will only cat 6*a*500, 4*a*100, 5*a*10 and 6*a. Instead of 3456 times "a".
    	 *
    	 * @param count expected repetition of s
    	 * @param s the String to be concatenated
    	 * @param prebuiltStrings optional pre-concatenated "s" strings, <b>in descent size order</b>.
    	 * @return s repeated count times
    	 */
    	public static StringBuilder repeat(int count, CharSequence s, String... prebuiltStrings) {
    		StringBuilder result = new StringBuilder(count * s.length());
    		repeat(result, count, s, prebuiltStrings);
    		return result;
    	}

    	/**
    	 * Concatenates count (positive) times parameter s.
    	 * Optional Strings in prebuildStrings are used to speed concatenation for large count numbers if you already have
    	 * large snippets of s pre-concatenated. For instance, you want 3456 times "a" and you already got constants with
    	 * a*500, a*100, a*10, then this method will only cat 6*a*500, 4*a*100, 5*a*10 and 6*a. Instead of 3456 times "a".
    	 *
    	 * @param result destination of the concatenation
    	 * @param count expected repetition of s
    	 * @param s the {@link CharSequence} to be concatenated to result
    	 * @param prebuiltStrings optional pre-concatenated "s" strings, <strong>in descent size order</strong>.
    	 * @return result with s repeated count times appended
    	 */
    	public static StringBuilder repeat(StringBuilder result, int count, CharSequence s, CharSequence... prebuiltStrings) {
    		result.ensureCapacity(result.length() + count * s.length());	// to avoid extra allocation cycles
    		int snippetCount, remainer = count;
    		for (CharSequence snippet : prebuiltStrings) {
    			int snippetLength = snippet.length();
    			snippetCount = remainer / snippetLength;
    			for (int i = 0; i < snippetCount; i++) {
    				result.append(snippet);
    			}
    			remainer = remainer % snippetLength;
    		}
    		for (int i = 0; i < remainer; i++) {
    			result.append(s);
    		}
    		return result;
    	}

    	/**
    	 * Equivalent to {@link String#split(String)} without regexp which is unnecessary for char separator
    	 * @param stringToBeSplit
    	 * @param separator
    	 * @param keepSeparatorInResult
    	 * @return a {@link String} split into pieces each time separator is found in it
    	 */
    	public static List<String> split(String stringToBeSplit, char separator, boolean keepSeparatorInResult) {
    		List<String> result = new ArrayList<>();
    		int separatorIndex = stringToBeSplit.indexOf(separator);
    		int previousSeparatorIndex = 0;
    		int substringPadding = keepSeparatorInResult ? 1 : 0;
    		while (separatorIndex != -1) {
    			result.add(stringToBeSplit.substring(previousSeparatorIndex, separatorIndex + substringPadding));
    			previousSeparatorIndex = separatorIndex + 1;
    			separatorIndex = stringToBeSplit.indexOf(separator, previousSeparatorIndex);
    		}
    		if (previousSeparatorIndex < stringToBeSplit.length()) {
    			result.add(stringToBeSplit.substring(previousSeparatorIndex));
    		}
    		return result;
    	}

    	public static CharSequence head(@Nullable CharSequence cs, @Nonnegative int headSize) {
    		return doWithDelegate(cs, new DefaultNullOrEmptyDelegate() {
    			@Override
    			public CharSequence onNotNullNotEmpty(CharSequence cs) {
    				return cs.subSequence(0, Math.min(cs.length(), headSize));
    			}
    		});
    	}

    	public static CharSequence head(@Nullable String cs, String untilIncluded) {
    		return doWithDelegate(cs, new DefaultNullOrEmptyDelegate() {
    			@Override
    			public CharSequence onNotNullNotEmpty(CharSequence cs) {
    				int index = ((String) cs).indexOf(untilIncluded);
    				return cs.subSequence(0, Math.min(cs.length(), Objects.fallback(index, -1, 0)));
    			}
    		});
    	}

    	public static CharSequence cutHead(@Nullable CharSequence cs, @Nonnegative int headSize) {
    		return doWithDelegate(cs, new DefaultNullOrEmptyDelegate() {
    			@Override
    			public CharSequence onNotNullNotEmpty(CharSequence cs) {
    				return cs.subSequence(Math.min(headSize, cs.length()), cs.length());
    			}
    		});
    	}

    	public static CharSequence tail(@Nullable CharSequence cs, @Nonnegative int tailSize) {
    		return doWithDelegate(cs, new DefaultNullOrEmptyDelegate() {
    			@Override
    			public CharSequence onNotNullNotEmpty(CharSequence cs) {
    				return cs.subSequence(Math.max(0, cs.length() - tailSize), cs.length());
    			}
    		});
    	}

    	/**
    	 * Gives the tailing {@link String} of a {@link CharSequence} that occurs after the last occurrence of given {@link String}
    	 * @param cs any {@link CharSequence}, even null
    	 * @param afterLast the occurring {@link String} to be found near the end
    	 * @return the tailing {@link String} of given {@link CharSequence} that occurs after the last occurrence of given {@link String}
    	 */
    	public static CharSequence tail(@Nullable CharSequence cs, String afterLast) {
    		return doWithDelegate(cs, new DefaultNullOrEmptyDelegate() {
    			@Override
    			public CharSequence onNotNullNotEmpty(CharSequence cs) {
    				return cs.subSequence(Math.max(0, cs.toString().lastIndexOf(afterLast) + afterLast.length()), cs.length());
    			}
    		});
    	}

    	public static CharSequence cutTail(@Nullable CharSequence cs, @Nonnegative int tailSize) {
    		return doWithDelegate(cs, new DefaultNullOrEmptyDelegate() {
    			@Override
    			public CharSequence onNotNullNotEmpty(CharSequence cs) {
    				return cs.subSequence(0, preventNegative(cs.length() - tailSize));
    			}
    		});
    	}

    	/**
    	 * Cuts a {@link CharSequence} and appends 3 dots ("...") at the end if its length is strictly greater than length
    	 *
    	 * @param cs any {@link CharSequence}
    	 * @param length length at which given {@link CharSequence} must be cut and appended "..."
    	 * @return length-firsts characters of given {@link CharSequence} appended with "...", therefore its size is length+3
    	 */
    	public static CharSequence ellipsis(@Nullable CharSequence cs, @Nonnegative int length) {
    		return doWithDelegate(cs, new DefaultNullOrEmptyDelegate() {
    			@Override
    			public CharSequence onNotNullNotEmpty(CharSequence cs) {
    				if (cs.length() > length) {
    					return cs.subSequence(0, length) + "...";
    				} else {
    					return cs;
    				}
    			}
    		});
    	}

    	/**
    	 * @param i any integer
    	 * @return 0 if i < 0
    	 */
    	private static int preventNegative(int i) {
    		return Math.max(i, 0);
    	}

    	private static CharSequence doWithDelegate(@Nullable CharSequence cs, INullOrEmptyDelegate emptyDelegate) {
    		if (cs == null) {
    			return emptyDelegate.onNull();
    		} else if (cs.length() == 0) {
    			return emptyDelegate.onEmpty();
    		} else {
    			return emptyDelegate.onNotNullNotEmpty(cs);
    		}
    	}

    	/**
    	 * Give a printable view of an object through method references of any of its properties. These will be concatenated to each other
    	 * with comma (", ").
    	 * Result of method references are printed by {@link StringBuilder#append(Object)} contract.
    	 *
    	 * @param object any object (not null)
    	 * @param printableProperties functions that give a properties to be concatenated
    	 * @param <O> object type
    	 * @return the concatenation of the results of functions invocation on the given object
    	 */
    	@SafeVarargs
    	public static <O> String footPrint(O object, Function<O, ?> ... printableProperties) {
    		StringAppender result = new StringAppender();
    		for (Function<O, ?> printableProperty : printableProperties) {
    			result.cat(printableProperty.apply(object), ", ");
    		}
    		return result.cutTail(2).toString();
    	}

    	private interface INullOrEmptyDelegate {
    		CharSequence onNull();
    		CharSequence onEmpty();
    		CharSequence onNotNullNotEmpty(CharSequence cs);
    	}

    	private static abstract class DefaultNullOrEmptyDelegate implements INullOrEmptyDelegate {
    		@Override
    		public CharSequence onNull() {
    			return null;
    		}

    		@Override
    		public CharSequence onEmpty() {
    			return "";
    		}
    	}

    	private Strings() {}
    }